1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   */
19  package org.codehaus.groovy.runtime;
20  
21  import groovy.lang.MetaMethod;
22  import groovy.lang.MetaProperty;
23  
24  import java.lang.reflect.Constructor;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.HashSet;
29  import java.util.LinkedList;
30  import java.util.List;
31  import java.util.Set;
32  
33  import org.codehaus.groovy.reflection.CachedClass;
34  import org.codehaus.groovy.reflection.ClassInfo;
35  
36  /**
37   * Utility class for MissingMethodException, MissingPropertyException etc.
38   * This class contains methods assisting in ranking and listing probable intended
39   * methods/fields when a exception is thrown.
40   *
41   * @author Hjalmar Ekengren
42   */
43  public class MethodRankHelper{
44      //These are the costs for the various edit operations
45      //they are used by the two DamerauLevenshtein implementations
46      public static final int DL_SUBSTITUTION = 10;
47      public static final int DL_DELETE = 10; //This is also the cost for a insert
48      public static final int DL_TRANSPOSITION = 5;
49      public static final int DL_CASE = 5;
50      
51      public static final int MAX_RECOMENDATIONS = 5;
52      public static final int MAX_METHOD_SCORE = 50;
53      public static final int MAX_CONSTRUCTOR_SCORE = 20;
54      public static final int MAX_FIELD_SCORE = 30;
55      private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
56  
57      private static final class Pair<U,V> {
58          private U u;
59          private V v;
60          public Pair(U u, V v){
61              this.u = u;
62              this.v = v;
63          }
64      }
65      
66      /**
67       * Returns a string detailing possible solutions to a missing method
68       * if no good solutions can be found a empty string is returned.
69       *
70       * @param methodName the name of the method that doesn't exist
71       * @param type the class on which the method is invoked
72       * @param arguments the arguments passed to the method
73       * @return a string with probable solutions to the exception
74       */
75      public static String getMethodSuggestionString(String methodName, Class type, Object[] arguments){
76          ClassInfo ci = ClassInfo.getClassInfo(type);
77          List<MetaMethod> methods = new ArrayList<MetaMethod>(ci.getMetaClass().getMethods());
78          methods.addAll(ci.getMetaClass().getMetaMethods());
79          List<MetaMethod> sugg = rankMethods(methodName,arguments,methods);
80          StringBuilder sb = new StringBuilder();
81          if (!sugg.isEmpty()){
82              sb.append("\nPossible solutions: ");
83              for(int i = 0; i < sugg.size(); i++) {
84                  if(i != 0) sb.append(", ");
85                  sb.append(sugg.get(i).getName()).append("(");
86                  sb.append(listParameterNames(sugg.get(i).getParameterTypes()));
87                  sb.append(")");
88              }
89          }
90          Class[] argumentClasses = getArgumentClasses(arguments);
91          List<Pair<Class,Class>> conflictClasses = getConflictClasses(sugg,argumentClasses);
92          if (!conflictClasses.isEmpty()){
93              sb.append("\nThe following classes appear as argument class and as parameter class, ");
94              sb.append("but are defined by different class loader:\n");
95              boolean first = true;
96              for(Pair<Class,Class> pair: conflictClasses) {
97                  if (!first) {
98                      sb.append(", ");
99                  } else {
100                     first = false;
101                 }
102                 sb.append(pair.u.getName()).append(" (defined by '");
103                 sb.append(pair.u.getClassLoader());
104                 sb.append("' and '");
105                 sb.append(pair.v.getClassLoader());
106                 sb.append("')");
107             }
108             sb.append("\nIf one of the method suggestions matches the method you wanted to call, ");
109             sb.append("\nthen check your class loader setup.");
110         }
111         return sb.toString();
112     }
113     
114     private static List<Pair<Class,Class>> getConflictClasses(List<MetaMethod> sugg, Class[] argumentClasses) {
115         List<Pair<Class,Class>> ret = new LinkedList<Pair<Class,Class>>();
116         Set<Class> recordedClasses = new HashSet<Class>();
117         for (MetaMethod method : sugg) {
118             Class[] para = method.getNativeParameterTypes();
119             for (Class aPara : para) {
120                 if (recordedClasses.contains(aPara)) continue;
121                 for (Class argumentClass : argumentClasses) {
122                     if (argumentClass == null) continue;
123                     if (argumentClass == aPara) continue;
124                     if (argumentClass.getName().equals(aPara.getName())) {
125                         ret.add(new Pair<Class, Class>(argumentClass, aPara));
126                     }
127                 }
128                 recordedClasses.add(aPara);
129             }
130         }
131         return ret;
132     }
133 
134     private static Class[] getArgumentClasses(Object[] arguments) {
135         Class[] argumentClasses = new Class[arguments.length];
136         for (int i=0; i<argumentClasses.length; i++) {
137             Object arg = arguments[i];
138             if (arg==null) continue;
139             argumentClasses[i] = arg.getClass();
140         }
141         return argumentClasses;
142     }
143 
144     /**
145      * Returns a string detailing possible solutions to a missing constructor
146      * if no good solutions can be found a empty string is returned.
147      *
148      * @param arguments the arguments passed to the constructor
149      * @param type the class on which the constructor is invoked
150      * @return a string with probable solutions to the exception
151      */
152     public static String getConstructorSuggestionString(Class type, Object[] arguments){
153         Constructor[] sugg = rankConstructors(arguments, type.getConstructors());
154         if(sugg.length >0){
155             StringBuilder sb = new StringBuilder();
156             sb.append("\nPossible solutions: ");
157             for(int i = 0; i < sugg.length; i++){
158                 if(i != 0) sb.append(", ");
159                 sb.append(type.getName()).append("(");
160                 sb.append(listParameterNames(sugg[i].getParameterTypes()));
161                 sb.append(")");
162             }
163             return sb.toString();
164         } else{
165             return "";
166         }
167     }
168     
169     /**
170      * Returns a string detailing possible solutions to a missing field or property
171      * if no good solutions can be found a empty string is returned.
172      *
173      * @param fieldName the missing field
174      * @param type the class on which the field is sought
175      * @return a string with probable solutions to the exception
176      */
177     public static String getPropertySuggestionString(String fieldName, Class type){
178         ClassInfo ci = ClassInfo.getClassInfo(type);
179         List<MetaProperty>  fi = ci.getMetaClass().getProperties();
180         List<RankableField> rf = new ArrayList<RankableField>(fi.size());
181         StringBuilder sb = new StringBuilder();
182         sb.append("\nPossible solutions: ");
183         
184         for(MetaProperty mp : fi) rf.add(new RankableField(fieldName, mp));
185         Collections.sort(rf);
186 
187         int i = 0;
188         for (RankableField f : rf) {
189             if (i > MAX_RECOMENDATIONS) break;
190             if (f.score > MAX_FIELD_SCORE) break;
191             if(i > 0) sb.append(", ");
192             sb.append(f.f.getName());
193             i++;
194         }
195         return i > 0? sb.toString(): "";
196     }
197     
198     /**
199      * creates a comma separated list of each of the class names.
200      *
201      * @param cachedClasses the array of Classes
202      * @return the Class names
203      */
204     private static String listParameterNames(Class[] cachedClasses){
205         StringBuilder sb = new StringBuilder();
206       for(int i =0; i < cachedClasses.length;i++){
207           if(i != 0) sb.append(", ");
208           sb.append(cachedClasses[i].getName());
209       }
210       return sb.toString();
211     }
212     
213     
214     private static String listParameterNames(CachedClass[] cachedClasses){
215         StringBuilder sb = new StringBuilder();
216         for(int i =0; i < cachedClasses.length;i++){
217             if(i != 0) sb.append(", ");
218             sb.append(cachedClasses[i].getName());
219         }
220         return sb.toString();
221       }
222     
223     /**
224      * Returns a sorted(ranked) list of a selection of the methods among candidates which
225      * most closely resembles original.
226      *
227      * @param name
228      * @param original
229      * @param methods
230      * @return a sorted lists of Methods
231      */
232     private static List<MetaMethod> rankMethods(String name, Object[] original, List<MetaMethod> methods) {
233         List<RankableMethod> rm = new ArrayList<RankableMethod>(methods.size());
234         if (original==null) original = EMPTY_OBJECT_ARRAY;
235         Class[] ta = new Class[original.length];
236     
237         Class nullC =  NullObject.class;
238         for(int i = 0; i < original.length; i++){
239             //All nulls have to be wrapped so that they can be compared
240             ta[i] = original[i] == null?nullC: original[i].getClass();
241         }
242 
243         for (MetaMethod m:methods) {
244             rm.add(new RankableMethod(name, ta, m));
245         }
246         Collections.sort(rm);
247         
248         List<MetaMethod> l =  new ArrayList<MetaMethod>(rm.size());
249         for (RankableMethod m : rm) {
250             if (l.size() > MAX_RECOMENDATIONS) break;
251             if (m.score > MAX_METHOD_SCORE) break;
252             l.add(m.m);
253         }
254         return l;
255     }
256 
257     /**
258      * This class wraps a method object and a score variable so methods 
259      * Can easily be ranked by their likeness to a another method
260      *
261      */
262     private static final class RankableMethod implements Comparable {
263         final MetaMethod m;
264         final Integer score;
265 
266         public RankableMethod(String name, Class[] argumentTypes, MetaMethod m2) {
267             this.m = m2;
268             int nameDist = delDistance(name, m2.getName());
269 
270             //unbox primitives
271             Class[] mArgs = new Class[m2.getParameterTypes().length];
272             for(int i =0; i < mArgs.length; i++){
273                 //All args have to be boxed since argumentTypes is always boxed
274                 mArgs[i] = boxVar(m2.getParameterTypes()[i].getTheClass());
275             }
276             int argDist = damerauLevenshteinDistance(argumentTypes,mArgs);
277             this.score = nameDist + argDist;
278         }
279 
280         public int compareTo(Object o) {
281             RankableMethod mo = (RankableMethod) o;
282             return score.compareTo(mo.score);
283         }
284     }
285 
286     /**
287      * Returns a sorted(ranked) list of a selection of the constructors among candidates which
288      * most closely resembles original.
289      *
290      * @param original
291      * @param candidates
292      * @return a sorted lists of Methods
293      */
294     private static Constructor[] rankConstructors(Object[] original, Constructor[] candidates) {
295         RankableConstructor[] rc = new RankableConstructor[candidates.length];
296         Class[] ta = new Class[original.length];
297 
298         Class nullC = NullObject.class;
299         for (int i = 0; i < original.length; i++) {
300             //All nulls have to be wrapped so that they can be compared
301             ta[i] = original[i] == null ? nullC : original[i].getClass();
302         }
303 
304         for (int i = 0; i < candidates.length; i++) {
305             rc[i] = new RankableConstructor(ta, candidates[i]);
306         }
307         Arrays.sort(rc);
308         List<Constructor> l = new ArrayList<Constructor>();
309         int index = 0;
310         while (l.size() < MAX_RECOMENDATIONS && index < rc.length && rc[index].score < MAX_CONSTRUCTOR_SCORE) {
311             l.add(rc[index].c);
312             index++;
313         }
314         return l.toArray(new Constructor[l.size()]);
315     }
316 
317     /**
318      * This class wraps a method object and a score variable so methods 
319      * Can easily be ranked by their likeness to a another method
320      *
321      */
322     private static final class RankableConstructor implements Comparable {
323         final Constructor c;
324         final Integer score;
325 
326         public RankableConstructor(Class[] argumentTypes, Constructor c) {
327             this.c = c;
328             //unbox primitives
329             Class[] cArgs = new Class[c.getParameterTypes().length];
330             for(int i =0; i < cArgs.length; i++){
331                 //All args have to be boxed since argumentTypes is always boxed
332                 cArgs[i] = boxVar(c.getParameterTypes()[i]);
333             }
334 
335             this.score = damerauLevenshteinDistance(argumentTypes,cArgs);
336         }
337 
338         public int compareTo(Object o) {
339             RankableConstructor co = (RankableConstructor) o;
340             return score.compareTo(co.score);
341         }
342     }
343     
344     /**
345      * This class wraps a method object and a score variable so methods 
346      * Can easily be ranked by their likeness to a another method
347      *
348      */
349     private static final class RankableField implements Comparable {
350         final MetaProperty f;
351         final Integer score;
352 
353         public RankableField(String name, MetaProperty mp) {
354             this.f = mp;
355             this.score = delDistance(name,mp.getName());
356         }
357 
358         public int compareTo(Object o) {
359             RankableField co = (RankableField) o;
360             return score.compareTo(co.score);
361         }
362     }
363     
364     /**
365      * If c is a primitive class this method returns a boxed version
366      * otherwise c is returned.
367      * In java 1.5 this can be simplified thanks to the Type class.
368      * @param c
369      * @return a boxed version of c if c can be boxed, else c
370      */
371     protected static Class boxVar(Class c){
372         if(Boolean.TYPE.equals(c)){
373             return Boolean.class;
374         }else if(Character.TYPE.equals(c)){
375             return Character.class;
376         }else if(Byte.TYPE.equals(c)){
377             return Byte.class;
378         }else if(Double.TYPE.equals(c)){
379             return Double.class;
380         }else if(Float.TYPE.equals(c)){
381             return Float.class;
382         }else if(Integer.TYPE.equals(c)){
383             return Integer.class;
384         }else if(Long.TYPE.equals(c)){
385             return Long.class;
386         }else if(Short.TYPE.equals(c)){
387             return Short.class;
388         }else{
389             return c;
390         }
391     }
392     
393     /**
394      * This is a small wrapper for nulls
395      */
396     private static class NullObject{
397     }
398 
399     /**
400      * This is a slightly modified version of the Damerau Levenshtein distance
401      * algorithm. It has a additional test to see if a character has switched case,
402      * in the original algorithm this counts as a substitution.
403      * The "cost" for a substitution is given as 10 instead of 1 in this version,
404      * this enables transpositions and case modifications to have a lower cost than
405      * substitutions.
406      *
407      * Currently the lowercase versions of t_j and s_i isn't cached, its probable
408      * that some speed could be gained from this.
409      * 
410      * This version is based on Chas Emerick's implementation of Levenshtein Distance
411      * for jakarta commons.
412      * @param s a CharSequence
413      * @param t the CharSequence to be compared to s
414      * @return a value representing the edit distance between s and t
415      */
416     public static int delDistance(CharSequence s, CharSequence t) {
417         if (s == null || t == null) {
418             throw new IllegalArgumentException("Strings must not be null");
419         }
420 
421         int n = s.length(); // length of s
422         int m = t.length(); // length of t
423 
424         if (n == 0) {
425             return m;
426         } else if (m == 0) {
427             return n;
428         }
429 
430         //we have to keep 3 rows instead of the 2 used in Levenshtein
431         int[][] vals = new int[3][n + 1];
432 
433 
434         int _d[]; //placeholder to assist in rotating vals
435 
436         // indexes into strings s and t
437         int i; // iterates through s
438         int j; // iterates through t
439 
440         char t_j; // jth character of t
441         char s_i; // ith character of s
442         int cost; // cost
443 
444         for (i = 0; i <= n; i++) {
445             vals[1][i] = i * DL_DELETE;
446         }
447 
448         for (j = 1; j <= m; j++) {
449             t_j = t.charAt(j - 1);
450             vals[0][0] = j * DL_DELETE;
451 
452             for (i = 1; i <= n; i++) {
453                 s_i = s.charAt(i - 1);
454                 if (Character.isLowerCase(s_i) ^ Character.isLowerCase(t_j)) {
455                     //if s_i and t_i don't have have the same case
456                     cost = caselessCompare(s_i, t_j) ? DL_CASE : DL_SUBSTITUTION;
457                 } else {
458                     //if they share case check for substitution
459                     cost = s_i == t_j ? 0 : DL_SUBSTITUTION;
460                 }
461 
462                 // minimum of cell to the left+1, to the top+1, diagonally left and up +cost
463                 vals[0][i] = Math.min(Math.min(vals[0][i - 1] + DL_DELETE, vals[1][i] + DL_DELETE), vals[1][i - 1] + cost);
464 
465                 //Check for transposition, somewhat more complex now since we have to check for case
466                 if (i > 1 && j > 1) {
467                     cost = Character.isLowerCase(s_i) ^ Character.isLowerCase(t.charAt(j - 2)) ? DL_CASE : 0;
468                     cost = Character.isLowerCase(s.charAt(i - 2)) ^ Character.isLowerCase(t_j) ? cost + DL_CASE : cost;
469 
470                     if (caselessCompare(s_i, t.charAt(j - 2)) && caselessCompare(s.charAt(i - 2), t_j)) {
471                         vals[0][i] = Math.min(vals[0][i], vals[2][i - 2] + DL_TRANSPOSITION + cost);
472                     }
473                 }
474             }
475 
476             // rotate all value arrays upwards(older rows get a higher index)
477             _d = vals[2];
478             vals[2] = vals[1];
479             vals[1] = vals[0];
480             vals[0] = _d;
481         }
482 
483         // our last action in the above loop was to rotate vals, so vals[1] now
484         // actually has the most recent cost counts
485         return vals[1][n];
486     }
487 
488     /**
489      * Compares two characters whilst ignoring case.
490      * @param a the first character
491      * @param b the second character
492      * @return true if the characters are equal
493      */
494     private static boolean caselessCompare(char a, char b){
495         return Character.toLowerCase(a) == Character.toLowerCase(b);
496     }
497     
498     /**
499      * This is a implementation of DL distance between two Object arrays instead
500      * of character streams. The objects are compared using their equals method.
501      * No objects may be null.
502      * This implementation is based on Chas Emerick's implementation of Levenshtein Distance
503      * for jakarta commons.
504      * @param s a Object array
505      * @param t this array is compared to s
506      * @return the edit distance between the two arrays
507      */
508     public static int damerauLevenshteinDistance(Object[] s, Object[] t) {
509         if (s == null || t == null) {
510             throw new IllegalArgumentException("Arrays must not be null");
511         }
512 
513         int n = s.length; // length of s
514         int m = t.length; // length of t
515 
516         if (n == 0) {
517             return m;
518         } else if (m == 0) {
519             return n;
520         }
521 
522         int[][] vals = new int[3][n + 1];
523 
524 
525         int _d[]; //placeholder to assist in rotating vals
526 
527         // indexes into arrays s and t
528         int i; // iterates through s
529         int j; // iterates through t
530 
531         Object t_j; // jth object of t
532 
533         int cost; // cost
534 
535         for (i = 0; i <= n; i++) {
536             vals[1][i] = i * DL_DELETE ;
537         }
538 
539         for (j = 1; j <= m; j++) {
540             t_j = t[j - 1];
541             vals[0][0] = j * DL_DELETE ;
542 
543             for (i = 1; i <= n; i++) {
544                 cost = s[i - 1].equals(t_j)? 0 : DL_SUBSTITUTION;
545                 // minimum of cell to the left+1, to the top+1, diagonally left and up +cost
546                 vals[0][i] = Math.min(Math.min(vals[0][i - 1] + DL_DELETE, vals[1][i] + DL_DELETE), vals[1][i - 1] + cost);
547 
548                 //Check for transposition
549                 if(i > 1 && j > 1 && s[i -1].equals(t[j -2]) && s[i- 2].equals(t_j)){
550                     vals[0][i] = Math.min(vals[0][i], vals[2][i-2] + DL_TRANSPOSITION);
551                 }
552             }
553 
554             // rotate all value arrays upwards(older rows get a higher index)
555             _d = vals[2];
556             vals[2] = vals[1];
557             vals[1] = vals[0];
558             vals[0] = _d;
559         }
560 
561         return vals[1][n];
562     }
563 }